﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.EventHandling;

using CacheWorkItemBucket = ScrumTable.DL.Data.Generic.Cache.CacheElementListEntryBucket<ScrumTable.DL.Data.Generic.Cache.CacheWorkItem>;
using CacheWorkItemStoreChangeArgs = ScrumTable.DL.Data.Generic.Cache.CacheElementListChangedEventArgs<ScrumTable.DL.Data.Generic.Cache.CacheWorkItem>;
using CacheWorkItemStoreChangeAttaArgs = ScrumTable.DL.Data.Generic.Cache.CacheElementListChangedEventArgs<ScrumTable.DL.Data.Generic.Cache.CacheAttachment>;

#endregion


namespace ScrumTable.DL.Data.Generic.Cache
{
    /// <summary>
    /// This class wraps provides a CacheElementDictionary list.
    /// </summary>
    public class CacheWorkItemStore : IEnumerable<CacheWorkItem>
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private readonly FastSmartWeakEvent<EventHandler<CacheWorkItemStoreChangeArgs>> _workItemChangeEvents = new FastSmartWeakEvent<EventHandler<CacheWorkItemStoreChangeArgs>>();
        private readonly FastSmartWeakEvent<EventHandler<CacheWorkItemStoreChangeAttaArgs>> _changeAttaEvents = new FastSmartWeakEvent<EventHandler<CacheWorkItemStoreChangeAttaArgs>>();

        private readonly CacheWorkItemStoreIndex _workItemIndex;
        private readonly CacheProject _ownerProject;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Returns an enumerator that iterates through the collection and returns the bucket with its unique id.
        /// </summary>
        internal IEnumerable<KeyValuePair<ValueChain<string>, CacheWorkItemBucket>> Buckets
        {
            get { return _workItemIndex.HierarchicalBuckets; }
        }

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new CacheElementDictionary instance. This constructor is used in
        /// the current library only.
        /// </summary>
        internal CacheWorkItemStore(IDictionary<int, CacheWorkItem> entries, CacheProject ownerProject)
        {
            PreCondition.AssertNotNull(ownerProject, "ownerProject");

            _ownerProject = ownerProject;
            _workItemIndex = new CacheWorkItemStoreIndex(ownerProject);

            AddRange(entries);
            CheckLinkIntegrity();
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
        /// </returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable<CacheWorkItem>)_workItemIndex).GetEnumerator();
        }

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
        /// </returns>
        IEnumerator<CacheWorkItem> IEnumerable<CacheWorkItem>.GetEnumerator()
        {
            return ((IEnumerable<CacheWorkItem>)_workItemIndex).GetEnumerator();
        }

        /// <summary>
        /// Clones the current and all identities below the current cache element
        /// and fills them into the target store.
        /// </summary>
        /// <param name="target">Specifies the store which should be filled with the identities.</param>
        public void CloneIdentities(CacheIdentityStore target)
        {
            _workItemIndex.ForEach(wi => wi.CloneIdentities(target));
        }

        /// <summary>
        /// Gets the children below the given parent id chain.
        /// </summary>
        /// <param name="parentIdChain">Id chain of the parent element.</param>
        /// <param name="type">Specifies the work item type of the elements to retrieve.</param>
        /// <returns>Returns the retrieved collection of children.</returns>
        public ICollection<CacheWorkItem> GetChildren(ValueChain<string> parentIdChain, CacheWorkItemType type)
        {
            PreCondition.AssertNotNull(parentIdChain, "parentIdChain");

            return _workItemIndex.TryGetByChain(parentIdChain, wi => wi.Type == type).ToList();
        }

        /// <summary>
        /// Gets the specified child below the given parent id chain.
        /// </summary>
        /// <param name="parentIdChain">Id chain of the parent element.</param>
        /// <param name="childId">Specifies the id of the child.</param>
        /// <param name="type">Type of the work item to retrieve.</param>
        /// <returns>Returns the retrieved item with the given work item type.</returns>
        public CacheWorkItem TryGetChild(ValueChain<string> parentIdChain, string childId, CacheWorkItemType type = null)
        {
            PreCondition.AssertNotNull(parentIdChain, "parentIdChain");
            PreCondition.AssertNotNull(childId, "childId");

            CacheWorkItem retrievedItem = _workItemIndex.TryGetByChain(parentIdChain, childId);

            if (retrievedItem != null)
            {
                // if a type is present, we have to validate the work item against it.
                if (type != null)
                {
                    if (retrievedItem.Type == type)
                    {
                        return retrievedItem;
                    }
                    return null;
                }
                
                // otherwise, the type is ignored and the found work item must be returned
                return retrievedItem;
            }
            return null;
        }

        /// <summary>
        /// Returns the work item with the given id.
        /// </summary>
        /// <param name="workItemId">Specifies the id of the work item to check.</param>
        /// <remarks>This method call is slow and should not be used in conjunction with large work item storages. It has a search factor of O(n).</remarks>
        public CacheWorkItem TryGetById(int workItemId)
        {
            return _workItemIndex.TryGetById(workItemId);
        }

        /// <summary>
        /// Returns the work item id chain of the work item with the given id.
        /// </summary>
        /// <param name="workItemId">Specifies the id of the work item to check.</param>
        public ValueChain<string> TryGetChainById(int workItemId)
        {
            return _workItemIndex.TryGetChainById(workItemId);
        }

        /// <summary>
        /// Adds the given element to the list.
        /// </summary>
        /// <param name="parentIdChain">Specifies the parent id chain.</param>
        /// <param name="toAdd">Specifies the element to add.</param>
        public void Add(ValueChain<string> parentIdChain, CacheWorkItem toAdd)
        {
            PreCondition.AssertNotNull(parentIdChain, "parentIdChain");
            PreCondition.AssertNotNull(toAdd, "toAdd");

            CacheWorkItemBucket bucket = CreateWorkItemBucket(toAdd, CacheElementListEntryState.New);
            RegisterWorkItem(bucket);
            _workItemIndex.RegisterInCacheTree(parentIdChain, bucket);

            OnWorkItemChange(new CacheWorkItemStoreChangeArgs
            {
                ChangeAction = CacheElementListChangeAction.Add,
                Bucket = bucket,
                ParentChain = parentIdChain
            });
        }

        /// <summary>
        /// Returns a new instance of a dictionary which contains the bucket clones of all changed items.
        /// </summary>
        /// <returns>
        /// Returns the dictionary with the cloned bucket instances.
        /// </returns>
        internal CacheElementChangeSet<ValueChain<string>, CacheWorkItem> ExtractChangedBuckets()
        {
            CacheElementChangeSet<ValueChain<string>, CacheWorkItem> changeSet =
                new CacheElementChangeSet<ValueChain<string>, CacheWorkItem>();

            _workItemIndex.HierarchicalBuckets.ForEach(
                chainBucketPair =>
                    {
                        if (chainBucketPair.Value.IsDirty)
                        {
                            changeSet.Add(chainBucketPair.Key, chainBucketPair.Value.Clone());

                            // reset state to unmodified. Caution: removing a workitem is not supported!
                            // TODO: (geh) Add remove from cache here, if WorkItem remove should be supported...
                            chainBucketPair.Value.Element.RemoveDirtyFlag();
                            chainBucketPair.Value.RemoveDirtyFlag();
                        }
                    });
            return changeSet;
        }

        private void AddRange(IDictionary<int, CacheWorkItem> entries)
        {
            if (entries == null)
                return;

            foreach (KeyValuePair<int, CacheWorkItem> workItemPair in entries)
            {
                EstablishRelationWithUniqueIds(workItemPair.Value, entries);

                CacheWorkItemBucket bucket = CreateWorkItemBucket(workItemPair.Value, CacheElementListEntryState.Unmodified);
                RegisterWorkItem(bucket);

                ValueChain<string> parentChain = _workItemIndex.RegisterInCacheTree(
                    index => entries.ContainsKey(index),
                    index => entries[index],
                    bucket);

                OnWorkItemChange(new CacheWorkItemStoreChangeArgs
                {
                    ChangeAction = CacheElementListChangeAction.Add,
                    Bucket = bucket,
                    ParentChain = parentChain
                });
            }
        }


        #region Merge Changes Procedures

        internal void MergeChanges(IEnumerable<CacheElementMergeInfo<CacheWorkItem>> changes)
        {
            PreCondition.AssertNotNull(changes, "changes");

            OnMergeChangesError(
                from change in changes
                where change.SaveFeedback.State == CacheSavePairState.Error
                select change);

            OnMergeChangesInvalidateData(
                from change in changes
                where change.SaveFeedback.State == CacheSavePairState.Updated
                select change);

            OnMergeChangesDeleted(
                from change in changes
                where change.SaveFeedback.State == CacheSavePairState.Deleted
                select change);

            OnMergeChangesSaved(
                from change in changes
                where change.SaveFeedback.State == CacheSavePairState.Saved
                select change);
        }

        private void ImportChangesOnBuckets(IEnumerable<CacheElementMergeInfo<CacheWorkItem>> changes)
        {
            changes.ForEach(mergeInfo => MergeBucket(mergeInfo.Bucket, mergeInfo.SaveFeedback));
        }

        /// <summary>
        /// Imports the accepted new id's into the cache element tree.
        /// </summary>
        /// <param name="changes">Specifies the elements to accept.</param>
        /// <remarks>The import avoids value tracking.</remarks>
        private void ImportDataChanges(IEnumerable<CacheElementMergeInfo<CacheWorkItem>> changes)
        {
            ImportNewWorkItemsIntoIndex(changes);
            ImportAndResolveIds(changes);
            ImportChangesOnBuckets(changes);
            CheckLinkIntegrity();
            AcceptChanges(changes);
        }

        /// <summary>
        /// Imports the failed changes. Caution: The id's must be also imported into the cache element tree.
        /// Otherwise the WorkItem may be created in back-end side multiple times.
        /// </summary>
        /// <param name="changes"></param>
        private void ImportFailedDataChanges(IEnumerable<CacheElementMergeInfo<CacheWorkItem>> changes)
        {
            ImportAndResolveIds(changes);
            ImportChangesOnBuckets(changes);
        }

        private void AcceptChanges(IEnumerable<CacheElementMergeInfo<CacheWorkItem>> changes)
        {
            changes.ForEach(
                elementToAccept =>
                    {
                        elementToAccept.SaveFeedback.Accepted = true;

                        if (_workItemIndex.Contains(elementToAccept.Bucket.Element.UniqueId))
                        {
                            _workItemIndex[elementToAccept.Bucket.Element.UniqueId].Element.ChangesAccepted();
                        }
                    });
        }

        private void ImportNewWorkItemsIntoIndex(IEnumerable<CacheElementMergeInfo<CacheWorkItem>> changes)
        {
            changes.ForEach(
                mergeInfo =>
                {
                    if (!_workItemIndex.Contains(mergeInfo.Bucket.Element.UniqueId))
                    {
                        _workItemIndex.RegisterInCacheIndex(mergeInfo.Bucket);
                    }
                });
        }

        private void ImportAndResolveIds(IEnumerable<CacheElementMergeInfo<CacheWorkItem>> changes)
        {
            // pre process workitem changes and index them by their unique ids
            IDictionary<string, ICacheIdentity> changedWorkItemIds = new Dictionary<string, ICacheIdentity>();
            changes.ForEach(chng => changedWorkItemIds[chng.Bucket.Element.UniqueId] = chng.Bucket.Element);

            // import all ids and revisions into cache
            foreach (CacheWorkItem workItem in _workItemIndex)
            {
                // check for new ids from back end system
                if (changedWorkItemIds.ContainsKey(workItem.UniqueId))
                {
                    int oldId = workItem.Id;
                    ICacheIdentity fromBackEndSystem = changedWorkItemIds[workItem.UniqueId];
                    workItem.Revision = fromBackEndSystem.Revision;

                    if (fromBackEndSystem.HasId && oldId != fromBackEndSystem.Id)
                    {
                        workItem.AssignId(fromBackEndSystem.Id);

                        // update corresponding cache element
                        _workItemIndex.UpdateId(oldId, fromBackEndSystem.Id, fromBackEndSystem.UniqueId);
                    }
                }
            }

            // import ids on child links (clean up link references)
            foreach (CacheWorkItem workItem in _workItemIndex)
            {
                ImportIdsOnLinks(workItem.ParentLinks);
                ImportIdsOnLinks(workItem.ChildLinks);
            }
        }

        private void ImportIdsOnLinks(CacheElementList<CacheWorkItemLink> links)
        {
            links.ForEach(
                link =>
                {
                    // assign related unique id, if a corresponding id is present
                    if (link.RelatedUnqiueId != CacheIdentity.DefaultUniqueId && _workItemIndex.Contains(link.RelatedUnqiueId))
                    {
                        link.RelatedId = _workItemIndex[link.RelatedUnqiueId].Element.Id;
                    }

                    // assign related id, if a corresponding id is present
                    if (link.RelatedId != CacheIdentity.DefaultId && _workItemIndex.Contains(link.RelatedId))
                    {
                        link.RelatedUnqiueId = _workItemIndex[link.RelatedId].Element.UniqueId;
                    }
                });
        }

        private void CheckLinkIntegrity()
        {
            // import all ids and revisions into cache
            foreach (CacheWorkItem workItem in _workItemIndex)
            {
                CheckLinkIntegrityOnLinks(workItem, workItem.ChildLinks, true);
                CheckLinkIntegrityOnLinks(workItem, workItem.ParentLinks, false);
            }
        }

        private void CheckLinkIntegrityOnLinks(CacheWorkItem parent, CacheElementList<CacheWorkItemLink> links, bool isChildCollection)
        {
            // this step is required to avoid inconistent link data between workitems
            links.ForEachIndexReverse(
                link =>
                    {
                        if (!_workItemIndex.Contains(link.RelatedUnqiueId)) // the given work item link does not exist anymore
                        {
                            links.ForceRemove(link, false);
                        }
                        else
                        {
                            CacheWorkItem relatedWorkItem = _workItemIndex[link.RelatedUnqiueId].Element;
                            CacheWorkItemLink linkToCurrent = _ownerProject.OwnerContext.ElementFactory.CreateWorkItemLink(parent.Id, parent.UniqueId);

                            CacheElementList<CacheWorkItemLink> relatedLinks =
                                isChildCollection
                                    ? relatedWorkItem.ParentLinks
                                    : relatedWorkItem.ChildLinks;

                            // restore related links, which have been partially removed only.
                            // this behaviour is required to restore links which have been imported
                            // from the back end system but only one link end has reported an update.
                            if (!relatedLinks.Contains(linkToCurrent))
                            {
                                CacheElementListEntryBucket<CacheWorkItemLink> refBucket = links.TryGetBucketByUniqueId(link.UniqueId);

                                relatedLinks.ForceAdd(
                                    linkToCurrent,
                                    false,
                                    refBucket.State,
                                    refBucket.OriginalState);
                            }
                        }
                    });
        }

        #endregion

        #region Import Bucket Change Procedures

        private void MergeBucket(CacheElementListEntryBucket<CacheWorkItem> toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            PreCondition.AssertNotNull(toMerge, "toMerge");
            PreCondition.AssertNotNull(saveProgressFeedback, "saveProgressFeedback");

            switch (saveProgressFeedback.State)
            {
                case CacheSavePairState.Saved:
                    OnMergeBucketChangeSaved(toMerge, saveProgressFeedback);
                    break;
                case CacheSavePairState.Deleted:
                    OnMergeBucketChangeDeleted(toMerge, saveProgressFeedback);
                    break;
                case CacheSavePairState.Updated:
                    OnMergeBucketChangeInvalidateData(toMerge, saveProgressFeedback);
                    break;
                case CacheSavePairState.Error:
                    OnMergeBucketChangeError(toMerge, saveProgressFeedback);
                    break;
                default: throw new ArgumentOutOfRangeException();
            }
        }

        private void ImportFailedBucketChanges(CacheElementListEntryBucket<CacheWorkItem> toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            CacheWorkItemBucket referencedBucketInCache = _workItemIndex.Contains(toMerge.Element.UniqueId)
                                                              ? _workItemIndex[toMerge.Element.UniqueId]
                                                              : null;

            if (referencedBucketInCache != null && referencedBucketInCache.Element.IsContextConnected)
            {
                if (toMerge.CacheRevision == referencedBucketInCache.CacheRevision)
                {
                    // if there were no change made during the save procedure. this condition may
                    // fail if the bucket has been deleted or overwritten
                    ImportBucketChangesOnExistingBucket(toMerge, saveProgressFeedback, referencedBucketInCache);
                }
            }
            else
            {
                // the element has been deleted but there's an error during the save procedure -> recreated it
                ImportBucketChangesOnNewBucket(toMerge, saveProgressFeedback);
            }
        }

        private void ImportBucketChanges(CacheElementListEntryBucket<CacheWorkItem> toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            /*
             * Elements may return in modified state. This state is for data export
             * to the back end system. If the element has been saved, this state could
             * be ignored because the elements are already stored or removed from the current list.
             */
            if (toMerge.State != CacheElementListEntryState.Unmodified)
            {
                return;
            }

            CacheWorkItemBucket referencedBucketInCache = _workItemIndex.Contains(toMerge.Element.UniqueId)
                                                              ? _workItemIndex[toMerge.Element.UniqueId]
                                                              : null;

            if (referencedBucketInCache != null && referencedBucketInCache.Element.IsContextConnected)
            {
                if (!referencedBucketInCache.CurrentIsDirty)
                {
                    ImportBucketChangesOnExistingBucket(toMerge, saveProgressFeedback, referencedBucketInCache);
                }
            }
            else
            {
                ImportBucketChangesOnNewBucket(toMerge, saveProgressFeedback);
            }
        }

        private void ForceImportBucketChanges(CacheElementListEntryBucket<CacheWorkItem> toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            CacheWorkItemBucket referencedBucketInCache = _workItemIndex.Contains(toMerge.Element.UniqueId)
                                                              ? _workItemIndex[toMerge.Element.UniqueId]
                                                              : null;

            if (referencedBucketInCache != null && referencedBucketInCache.Element.IsContextConnected)
            {
                ImportBucketChangesOnExistingBucket(toMerge, saveProgressFeedback, referencedBucketInCache);
            }
            else
            {
                ImportBucketChangesOnNewBucket(toMerge, saveProgressFeedback);
            }
        }

        private void ImportBucketChangesOnExistingBucket(CacheElementListEntryBucket<CacheWorkItem> toMerge, ICacheSaveProgressFeedback saveProgressFeedback, CacheElementListEntryBucket<CacheWorkItem> referencedBucketInCache)
        {
            // forward the changes to the bucket child elements
            referencedBucketInCache.MergeChanges(toMerge, saveProgressFeedback);
        }

        private void ImportBucketChangesOnNewBucket(CacheElementListEntryBucket<CacheWorkItem> toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            // a new element has been created on server side, let's create it in the cache too
            CacheWorkItemBucket bucket = CreateWorkItemBucket();
            bucket.MergeChanges(toMerge, saveProgressFeedback);
            RegisterWorkItem(bucket);
            ValueChain<string> parentChain = _workItemIndex.RegisterInCacheTree(bucket);

            OnWorkItemChange(new CacheWorkItemStoreChangeArgs
            {
                ChangeAction = CacheElementListChangeAction.Add,
                Bucket = bucket,
                ParentChain = parentChain
            });
        }

        #endregion

        #region Add Range (first data import) procedures

        private void RegisterWorkItem(CacheWorkItemBucket toImport)
        {
            // TODO: (geh) Remove the += registrations if a workitem is removed from the WorkItemStore
            toImport.Element.ParentLinksIntegrityInvalidate += OnWorkItemLinkIntegrityInvalidate;
            toImport.Element.ChildLinksIntegrityInvalidate += OnWorkItemLinkIntegrityInvalidate;
            toImport.Element.IterationPathIntegrityInvalidate += OnWorkItemIterationPathIntegrityInvalidate;

            toImport.Element.AttachmentsChange += OnWorkItemAttachmentChangeNotification;
            toImport.Element.Parent = _ownerProject;            
        }

        private void EstablishRelationWithUniqueIds(CacheWorkItem toImport, IDictionary<int, CacheWorkItem> workItemPool)
        {
            // fill child element unique id's
            foreach (CacheWorkItemLink cacheWorkItemLink in toImport.ChildLinks)
            {
                if (workItemPool.ContainsKey(cacheWorkItemLink.RelatedId))
                    cacheWorkItemLink.RelatedUnqiueId = workItemPool[cacheWorkItemLink.RelatedId].UniqueId;
            }

            // fill parent element unique id
            CacheWorkItemLink parentLink = toImport.ParentLink;

            if (parentLink != null && workItemPool.ContainsKey(parentLink.RelatedId))
            {
                toImport.ParentLink.RelatedUnqiueId = workItemPool[parentLink.RelatedId].UniqueId;
            }
        }

        private CacheWorkItemBucket CreateWorkItemBucket(CacheWorkItem element, CacheElementListEntryState initialState)
        {
            CacheWorkItemBucket bucket = CreateWorkItemBucket();
            bucket.Element = element;
            bucket.OriginalState = initialState;
            bucket.State = initialState;
            return bucket;
        }

        private CacheWorkItemBucket CreateWorkItemBucket()
        {
            return new CacheWorkItemBucket();
        }

        #endregion

        #region OnChange Event Handling Methods

        private void ChangeWorkItemIndexWithEvents(string uniqueElementId, Action toCall)
        {
            if (_workItemIndex.Contains(uniqueElementId))
            {
                FireChangeWorkItemIndexBeforeEvent(uniqueElementId);
            }

            toCall();
            FireChangeWorkItemIndexAfterEvent(uniqueElementId);
        }

        private void FireChangeWorkItemIndexBeforeEvent(string uniqueElementId)
        {
            OnWorkItemChange(
                new CacheWorkItemStoreChangeArgs
                {
                    Bucket = _workItemIndex[uniqueElementId],
                    ChangeAction = CacheElementListChangeAction.Remove,
                    ParentChain = _workItemIndex.TryGetChainById(uniqueElementId).ParentChain
                });
        }

        private void FireChangeWorkItemIndexAfterEvent(string uniqueElementId)
        {
            OnWorkItemChange(
                new CacheWorkItemStoreChangeArgs
                {
                    Bucket = _workItemIndex[uniqueElementId],
                    ChangeAction = CacheElementListChangeAction.Add,
                    ParentChain = _workItemIndex.TryGetChainById(uniqueElementId).ParentChain
                });
        }

        #endregion

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        /// <summary>
        /// This event will be fired if a WorkItem has been added / removed from the current store.
        /// </summary>
        internal event EventHandler<CacheWorkItemStoreChangeArgs> WorkItemChange
        {
            add { _workItemChangeEvents.Add(value); }
            remove { _workItemChangeEvents.Remove(value); }
        }

        /// <summary>
        /// Fires the Change (element add / remove) event.
        /// </summary>
        /// <param name="args">Specifies the event args which should be passed to the event listeners.</param>
        private void OnWorkItemChange(CacheWorkItemStoreChangeArgs args)
        {
            if (_ownerProject.OwnerContext.PropagateEvents)
            {
                _workItemChangeEvents.Raise(this, args);
            }
        }


        /// <summary>
        /// This event will be fired if an attachment has been added / removed from the current store.
        /// </summary>
        internal event EventHandler<CacheWorkItemStoreChangeAttaArgs> AttachmentChange
        {
            add { _changeAttaEvents.Add(value); }
            remove { _changeAttaEvents.Remove(value); }
        }

        /// <summary>
        /// Fires the AttachmentChange (element add / remove) event.
        /// </summary>
        /// <param name="args">Specifies the event args which should be passed to the event listeners.</param>
        private void OnAttachmentChange(CacheWorkItemStoreChangeAttaArgs args)
        {
            if (_ownerProject.OwnerContext.PropagateEvents)
            {
                _changeAttaEvents.Raise(this, args);
            }
        }


        /// <summary>
        /// An error occurred while saving the given cache element. This means, that
        /// the given element must be merged back (if there are no changes) into
        /// the cache tree.
        /// </summary>
        /// <param name="toMerge">Specifies the cache element which caused the error.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback provided by the data driver.</param>
        protected virtual void OnMergeBucketChangeError(CacheElementListEntryBucket<CacheWorkItem> toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            ImportFailedBucketChanges(toMerge, saveProgressFeedback);
        }

        /// <summary>
        /// An element on server side has changed (which behaviour may caused an error). Its
        /// cache data must be invalidated in order to resynchronize with the server.
        /// </summary>
        /// <param name="toMerge">Specifies the cache element which data should be invalidated.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback provided by the data driver.</param>
        protected virtual void OnMergeBucketChangeInvalidateData(CacheElementListEntryBucket<CacheWorkItem> toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            ForceImportBucketChanges(toMerge, saveProgressFeedback);
        }

        /// <summary>
        /// An element has saved on server side. This means that data in the local storage may
        /// be invalidated if the change caused additional changes on server side. Workflow
        /// changes may cause additional value changes.
        /// </summary>
        /// <param name="toMerge">Specifies the cache element which data have ben saved.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback provided by the data driver.</param>
        protected virtual void OnMergeBucketChangeSaved(CacheElementListEntryBucket<CacheWorkItem> toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            ImportBucketChanges(toMerge, saveProgressFeedback);
        }

        /// <summary>
        /// An element on server side has been deleted. The element in the cache must also be 
        /// deleted.
        /// </summary>
        /// <param name="toMerge">Specifies the cache element which has been deleted.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback provided by the data driver.</param>
        protected virtual void OnMergeBucketChangeDeleted(CacheElementListEntryBucket<CacheWorkItem> toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            ImportBucketChanges(toMerge, saveProgressFeedback);
        }


        /// <summary>
        /// An error occurred while saving the given cache elements. This means, that
        /// the given elements must be merged back (if there are no changes) into
        /// the cache tree.
        /// </summary>
        /// <param name="changes">Specifies the cache elements which caused the error.</param>
        private void OnMergeChangesError(IEnumerable<CacheElementMergeInfo<CacheWorkItem>> changes)
        {
            ImportFailedDataChanges(changes);
        }

        /// <summary>
        /// Elements on server side have changed (which behaviour may caused an error). Its
        /// cache data must be invalidated in order to resynchronize with the server.
        /// </summary>
        /// <param name="changes">Specifies the cache elements which data should be invalidated.</param>
        private void OnMergeChangesInvalidateData(IEnumerable<CacheElementMergeInfo<CacheWorkItem>> changes)
        {
            ImportDataChanges(changes);
        }

        /// <summary>
        /// Elements have been saved on server side. This means that data in the local storage may
        /// be invalidated if the change caused additional changes on server side. Workflow
        /// changes may cause additional value changes.
        /// </summary>
        /// <param name="changes">Specifies the cache elements which data have ben saved.</param>
        private void OnMergeChangesSaved(IEnumerable<CacheElementMergeInfo<CacheWorkItem>> changes)
        {
            ImportDataChanges(changes);
        }

        /// <summary>
        /// Elements on server side have been deleted. The elements in the cache must also be 
        /// deleted.
        /// </summary>
        /// <param name="changes">Specifies the cache elements which has been deleted.</param>
        private void OnMergeChangesDeleted(IEnumerable<CacheElementMergeInfo<CacheWorkItem>> changes)
        {
            ImportChangesOnBuckets(changes);
        }


        private void OnWorkItemIterationPathIntegrityInvalidate(object sender, EventArgs e)
        {
            string wiUniqueId = ((CacheWorkItem)sender).UniqueId;

            _workItemIndex.UpdateHierarchyMove(
                wiUniqueId,
                () => FireChangeWorkItemIndexBeforeEvent(wiUniqueId),
                () => FireChangeWorkItemIndexAfterEvent(wiUniqueId));
        }

        private void OnWorkItemAttachmentChangeNotification(object sender, CacheWorkItemAttachmentChangedEventArgs changeArgs)
        {
            OnAttachmentChange(
                new CacheWorkItemStoreChangeAttaArgs
                    {
                        Bucket = changeArgs.InnerEvent.Bucket,
                        ChangeAction = changeArgs.InnerEvent.ChangeAction,
                        ParentChain = _workItemIndex.TryGetChainById(((CacheWorkItem) sender).Id)
                    } );
        }

        private void OnWorkItemLinkIntegrityInvalidate(object sender, CacheWorkItemLinkChangedEventArgs changeArgs)
        {
            switch (changeArgs.InnerEvent.ChangeAction)
            {
                case CacheElementListChangeAction.Add:
                    {
                        // update link collections
                        OnWorkItemLinkAdded(new UpdateContext(this, (CacheWorkItem)sender, changeArgs));
                        break;
                    }
                case CacheElementListChangeAction.Remove:
                    {
                        // update link collections
                        OnWorkItemLinkRemoved(new UpdateContext(this, (CacheWorkItem)sender, changeArgs));
                        break;
                    }
            }
        }

        private void OnWorkItemLinkRemoved(UpdateContext updateCtx)
        {
            if (_ownerProject.OwnerContext.IsInMergeProcess)
            {
                updateCtx.ReferencedWorkItemLinkCollection.ForceRemove(updateCtx.CurrentWorkItemLink, false);
            }
            else
            {
                updateCtx.ReferencedWorkItemLinkCollection.Remove(updateCtx.CurrentWorkItemLink, false);
            }

            // update cache indexes, fire event to layers above
            if (updateCtx.ChangedCollection == CacheWorkItemLinkCollection.Child)
            {
                ChangeWorkItemIndexWithEvents(
                    updateCtx.ReferencedWorkItem.UniqueId,
                    () => _workItemIndex.UpdateHierarchyRemove(updateCtx.ReferencedWorkItem.UniqueId));
            }
            else
            {
                ChangeWorkItemIndexWithEvents(
                    updateCtx.CurrentWorkItem.UniqueId,
                    () => _workItemIndex.UpdateHierarchyRemove(updateCtx.CurrentWorkItem.UniqueId));
            }
        }

        private void OnWorkItemLinkAdded(UpdateContext updateCtx)
        {
            if (_ownerProject.OwnerContext.IsInMergeProcess)
            {
                // there's only one parent link allowed, remove existing link
                RemoveExistingWorkItemParentLink(
                    updateCtx,
                    childToRemoveOnCurrent => updateCtx.CurrentWorkItemLinkCollection.ForceRemove(childToRemoveOnCurrent, true),
                    childToRemoveOnReferenced => updateCtx.ReferencedWorkItemLinkCollection.ForceRemove(childToRemoveOnReferenced, true));

                // insert new link
                updateCtx.ReferencedWorkItemLinkCollection.ForceAdd(updateCtx.CurrentWorkItemLink, false);
            }
            else
            {
                // there's only one parent link allowed, remove existing link
                RemoveExistingWorkItemParentLink(
                    updateCtx,
                    childToRemoveOnCurrent => updateCtx.CurrentWorkItemLinkCollection.Remove(childToRemoveOnCurrent, true),
                    childToRemoveOnReferenced => updateCtx.ReferencedWorkItemLinkCollection.Remove(childToRemoveOnReferenced, true));

                // insert new link, fire event to layers above
                updateCtx.ReferencedWorkItemLinkCollection.Add(updateCtx.CurrentWorkItemLink, false);
            }

            // update cache indexes
            if (updateCtx.ChangedCollection == CacheWorkItemLinkCollection.Child)
            {
                ChangeWorkItemIndexWithEvents(
                    updateCtx.ReferencedWorkItem.UniqueId,
                    () => _workItemIndex.UpdateHierarchyAdd(updateCtx.ReferencedWorkItem.UniqueId));
            }
            else
            {
                ChangeWorkItemIndexWithEvents(
                    updateCtx.CurrentWorkItem.UniqueId,
                    () => _workItemIndex.UpdateHierarchyAdd(updateCtx.CurrentWorkItem.UniqueId));
            }
        }

        private void RemoveExistingWorkItemParentLink(UpdateContext updateCtx, Action<CacheWorkItemLink> removeActionOnCurrentCollection, Action<CacheWorkItemLink> removeActionOnReferencedCollection)
        {
            // there's only one parent link allowed, remove existing link
            if (updateCtx.ChangedCollection == CacheWorkItemLinkCollection.Parent)
            {
                RemoveWorkItemLinkIfAvailable(
                    updateCtx.ReferencedWorkItemLink,
                    updateCtx.CurrentWorkItemLinkCollection,
                    removeActionOnCurrentCollection);
            }
            else
            {
                RemoveWorkItemLinkIfAvailable(
                    updateCtx.CurrentWorkItemLink,
                    updateCtx.ReferencedWorkItemLinkCollection,
                    removeActionOnReferencedCollection);
            }
        }

        private void RemoveWorkItemLinkIfAvailable(CacheWorkItemLink currentWorkItemLink, CacheElementList<CacheWorkItemLink> listToModify, Action<CacheWorkItemLink> removeAction)
        {
            CacheWorkItemLink currentParentLink = listToModify.FirstAvailable();

            if (currentParentLink != null && currentParentLink != currentWorkItemLink)
            {
                // this call must fire the remove event in order to update
                // the other link side.
                removeAction(currentParentLink);
            }
        }

        #endregion



        private class UpdateContext
        {
            internal CacheWorkItemLinkCollection ChangedCollection { get; private set; }

            internal CacheWorkItemLink ReferencedWorkItemLink { get; private set; }
            internal CacheElementList<CacheWorkItemLink> ReferencedWorkItemLinkCollection { get; private set; }
            internal CacheWorkItem ReferencedWorkItem { get; private set; }

            internal CacheWorkItemLink CurrentWorkItemLink { get; private set; }
            internal CacheWorkItem CurrentWorkItem { get; private set; }
            internal CacheElementList<CacheWorkItemLink> CurrentWorkItemLinkCollection { get; private set; }

            internal UpdateContext(CacheWorkItemStore store, CacheWorkItem currentWorkItem, CacheWorkItemLinkChangedEventArgs changeArgs)
            {
                ChangedCollection = changeArgs.Collection;

                ReferencedWorkItem = store._workItemIndex[changeArgs.InnerEvent.Bucket.Element.RelatedUnqiueId].Element;
                ReferencedWorkItemLinkCollection =
                    (ChangedCollection == CacheWorkItemLinkCollection.Child)
                        ? ReferencedWorkItem.ParentLinks
                        : ReferencedWorkItem.ChildLinks;
                ReferencedWorkItemLink = store._ownerProject.OwnerContext.ElementFactory.CreateWorkItemLink(
                    ReferencedWorkItem.Id,
                    ReferencedWorkItem.UniqueId);

                CurrentWorkItem = currentWorkItem;
                CurrentWorkItemLinkCollection =
                    (ChangedCollection == CacheWorkItemLinkCollection.Parent)
                        ? CurrentWorkItem.ParentLinks
                        : CurrentWorkItem.ChildLinks;
                CurrentWorkItemLink = store._ownerProject.OwnerContext.ElementFactory.CreateWorkItemLink(
                    CurrentWorkItem.Id,
                    CurrentWorkItem.UniqueId);
            }
        }
    }
}